Skip to content

fix(spec,cli): walk a slotted page's slots and a tabs panel's items[].children, and give dashboard global filters a bundle group - #16883

Merged
os-zhuang merged 9 commits into
mainfrom
claude/issue-16772-i18n-unaddressable-surfaces
Sep 9, 2026
Merged

fix(spec,cli): walk a slotted page's slots and a tabs panel's items[].children, and give dashboard global filters a bundle group#16883
os-zhuang merged 9 commits into
mainfrom
claude/issue-16772-i18n-unaddressable-surfaces

Conversation

@claude

@claude claude Bot commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

Fixes #16772

Clause-②: yes

Container & model: M, mode:cloud, model: claude-fable-5-1 (CONTRACT_REVIEW_TIER, passed explicitly on the resumption dispatch). node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --tier packages/spec/src/system/i18n-resolver.ts packages/spec/src/system/translation.zod.ts packages/spec/api-surface/system.json packages/cli/src/utils/i18n-extract.ts, run by the dispatching seat for THIS dispatch, printed a "Clause ② SUSPECT surface" block naming packages/spec/src/system/i18n-resolver.ts and .../translation.zod.ts under packages/spec/src/** — "the contract surface … the normal landing zone of a clause-② card" — plus "whichever tier is dispatched, the PR's actual diff passes the clause-② enqueue gate before the card may enqueue." This diff moves packages/spec/api-surface/system.json (+3) ⇒ it widens the published surface ⇒ 强制条款② ⇒ fable.

What changed

Two surfaces the console renders that no translation bundle could address, both in packages/spec, both the benign direction of the seam #16735 describes (nothing to address, so nothing claimed coverage).

Finding A — the shared page walk now reaches a slotted page and a tabs panel. walkAddressedPageComponents (the ONE traversal behind translatePage and the CLI extractor) rooted at regions[].components[] only and descended properties.children only. Now:

  • roots: regions[].components[] AND slots.SLOT (PageSchema.slots declares each slot as one PageComponentSchema or an array of them — the walk accepts both and passes any other value through). Regions first, then slots in authored key order; both are root level (depth 0, nested: false) for the ruled id arbitration and for the page-name page:header route, so a slotted page's slots.header is translated as THE page's header by page name, and the id route is not read for it (the same exception a region-level header already had).
  • descent: properties.children AND a page:tabs / page:accordion panel's properties.items[].children — matched by SHAPE (an object entry of items carrying a children array), because properties is an open bag. The panel object itself is not a component and is not visited; its children sit one level below the tabs node. body / footer stay undescended (renderer back-compat fallback, not an authorable spelling). Depth cap, cycle guard and the translatePage stops at region-level components, so copy on components nested in another component's properties.children is authorable but never resolved — 4 KPI labels stay English on hotCRM's zh-CN landing page #12961 collision arbitration are unchanged.
  • signature: the parameter is AddressedPageRoots (a Pick of PageLike on regions and slots) instead of a Pick on regions alone, and the walk returns the rebuilt roots pair { regions, slots } (each key present exactly when present on the input) instead of the regions array. PageLike gains slots; translatePage carries the rebuilt slots back onto the document. In-repo consumers: translatePage (updated) and the CLI extractor (enumeration-only, ignores the return). objectui at the pinned .objectui-sha (53ded82b) imports none of walkAddressedPageComponents / AddressedPageRoots / GlobalFilterLike / globalFilterKey — measured with git grep at that commit, positive control pickLocalized hit.

Finding B — dashboards.NAME.globalFilters.KEY is a new bundle group (translation.zod.ts): label plus an options map from the option value spelled as a string to its translated label. The KEY is the filter's name, else its field — not a lenient fallback: GlobalFilterSchema.name is declared as "Stable filter name (variable key); defaults to field", so a filter that authors no name IS keyed by its field everywhere the platform reads it. The exported globalFilterKey() is the one derivation both the resolver and the extractor use. Alias tables, re-measured at the head: at the DASHBOARD level filters / globalFilterglobalFilters (translation.zod.ts:703), which do mirror DashboardSchema's own. INSIDE the group (:785) the table is name / title / textlabel and choices / values / itemsoptions, of which titlelabel and choices / valuesoptions are the GlobalFilterSchema aliases (dashboard.zod.ts:787); textlabel is the OPTION ROW's alias (:855), and name / items are not dashboard aliases at all. Every one of them is hint-only on a strictObject — an alias widens no accept set, it only names the one spelling the bundle takes — so the extra two are a cosmetic overreach in the hint table, recorded and not fixed here. optionsFrom options are fetched rows and deliberately have no key. translateDashboard overlays the group on the served document; only filters the bundle addresses are rebuilt, and globalFilters is left off the copy when none moved.

@objectstack/cli (i18n-extract.ts): collectExpectedEntries offers dashboards.NAME.globalFilters.KEY.label / .options.VALUE for every static filter, and the page-name header route (pages.NAME.title / .subtitle) for a page:header at ANY root by asking the shared walk (nested: false) instead of a hand loop over page.regions that would have offered nothing for slots.header. Component keys under slots and tab panels follow from the shared walk with no extractor change.

Bundles born under the ratchet: @objectstack/platform-objects Setup bundles (en, zh-CN, ja-JP, es-ES) gain dashboards.system_overview.globalFilters.created_at.label (the date-range bar; the filter authors no name, so it is keyed by field); the showcase example bundle gains its two dashboards' filter rows in en and zh-CN.

Docs / generated: content/docs/ui/translations.mdx (hand-written tree) gains the two key rows; content/docs/references/**, api-surface/system.json (+3: AddressedPageRoots, GlobalFilterLike, globalFilterKey), export-origins/system.json, liveness/translation.json and the strictness-ledger counts file were regenerated by the repo's own tooling (check:generated: all 15 artifacts up to date after the origin/main merge). The platform-objects extract-config note that explained its missing pages key by the old root set was re-measured: the walk now reaches every component on the three shipped record pages, none of them carries an id, so the extractor still offers the page label alone (the CLI boundary pin holds both halves: reached: true, addressed: 0, offered: ['label']).

Files outside packages/spec — nine hand-authored, plus the changeset, one hand-written doc page and three regenerated artifacts:

File
packages/cli/src/utils/i18n-extract.ts the globalFilters emitter + the root-header route through the shared walk
packages/cli/test/i18n-extract.test.ts NEW — the emitter pin (see below)
packages/cli/test/platform-page-i18n-parity.test.ts the CLI↔resolver boundary pin, re-measured on the widened walk
packages/platform-objects/scripts/i18n-extract.config.ts the note explaining the missing pages key, re-measured
packages/platform-objects/src/apps/translations/{en,zh-CN,ja-JP,es-ES}.ts 4 shipped Setup bundles — the new system_overview filter row
examples/app-showcase/src/system/translations/index.ts the showcase bundle's two dashboards' filter rows
content/docs/ui/translations.mdx hand-written key tree — the two new rows
.changeset/i18n-slotted-pages-and-global-filters.md the changeset
content/docs/references/api/protocol.mdx, content/docs/references/system/translation.mdx, docs/audits/2026-07-unknown-key-strictness-ledger.counts.md regenerated by the repo's own tooling

The other seven files are packages/spec: src/system/{i18n-resolver.ts, translation.zod.ts} and their two test files, plus the regenerated api-surface/system.json, export-origins/system.json and liveness/translation.json.

Changeset: .changeset/i18n-slotted-pages-and-global-filters.md@objectstack/spec minor, @objectstack/cli minor, @objectstack/platform-objects minor (:2-4). All three move published source and all three widen it, so all three are graded the same: spec (the new authorable group, +3 exports, the signature), cli (the new emitted globalFilters key family and the root-header route), platform-objects (new rows in four shipped Setup bundles). The LEVEL axis of scripts/check-changeset-no-major.mjs requires at least one moved package at minor+ under Clause-②: yes, and the WHICH-LEVEL rule grades a purely additive widening of a published surface at least minor per package. major is refused during the launch window ("we ship breaking changes as minor"), so the version number carries nothing about breaking-ness and the breaking half is carried in writing instead — three carriers, all in that file:

  • the **BREAKING** (return shape) banner at :9: walkAddressedPageComponents is a published export of @objectstack/spec and now returns the rebuilt roots pair { regions?, slots? } where it returned the regions array alone. The migration is on the same line — a caller that only enumerates through the visitor is unaffected; a caller that reads the return value binds const { regions } = walkAddressedPageComponents(doc, visit).
  • exactly ONE ADR-0087 disposition marker at :24: not-required (no-migration-prescription), argued at :22. Nothing an author writes moves — the group is additive, no spec key is retired, no stored sys_metadata shape changes — so objectstack migrate meta has nothing to act on; the one incompatible surface is a published function's TypeScript return type, which reaches every consumer through the compiler. (type-surface-only, the one category exempt from the prescription refusal, is closed to this PR: its predicate excludes a diff touching packages/spec/**.)
  • the new pin packages/cli/test/i18n-extract.test.ts:278-366, four cases over collectExpectedEntries: the key is name-else-field and every static option is offered; the source values carry through and an option is keyed by its value; nothing is offered under a field a named filter overrode, and nothing at all for an unkeyed filter; an optionsFrom filter gets its label and no option key. check:i18n-walk-parity measures at top-level group granularity and dashboards was already walked before this group existed, so that gate cannot see a sub-group emitter drift — this pin is what holds it.

check:api-surface reports 3 added / 0 breaking because its signatures snapshot is scoped to the defineX factories.

Resumption note

The predecessor session died on an account usage limit holding "two spec assertions failed and the CLI file failed at load". Re-run on the inherited HEAD f3e92ee1c (dist built 11:28Z, after the last wip commit at 11:25Z) BEFORE any change: i18n-resolver.test.ts + translation.test.ts 391/391 pass; the whole spec suite 465 files / 12978 tests pass; packages/cli/test/platform-page-i18n-parity.test.ts 27/27 pass. The failure bodies could not be reproduced on the committed tree — they belonged to a transient state (uncommitted edits or a pre-build dist) that no longer exists. The three wip commits were re-read adversarially against the schema premises (PageSchema.slots union per slot, GlobalFilterSchema.name default, PageTabsProps.items[]) and stand; the one thing they missed was the platform-objects changeset entry, added here.

Verification

Tree: origin/main c930f8597 merged via bash scripts/pm/os-regen-merge.sh (merge commit 718bff099; step 2 had no both-sides regen path, step 3 nothing extra to commit); pnpm install --frozen-lockfile; spec rebuilt; CLI dependency closure rebuilt (turbo, 56 tasks). Final commit af0593780 (comment + changeset only; no test input). origin/main has since moved 8 more commits to a5d4e286b (scripts/check-cli-test-child-env.mjs among them) — not re-merged here; the queue rebuild covers it.

Post-merge, all through scripts/pm/os-verify-lock.sh, verdicts quoted from its VERDICT command-exit line:

  • pnpm --filter @objectstack/spec testVERDICT command-exit 0 · 465 files / 12989 tests passed (at 718bff099)
  • pnpm --filter @objectstack/spec typecheck — exit 0 (tsc + scripts + check:test-typecheck: OK)
  • pnpm --filter @objectstack/cli exec vitest run --project unitVERDICT command-exit 0 · 186 files, 2556 passed | 6 expected fail; pnpm --filter @objectstack/cli typecheck OK. The integration tier is declared to CI (the diff touches no integration file or spawn entry).
  • pnpm --filter @objectstack/platform-objects test — 37 files / 545 passed; typecheck OK.
  • pnpm --filter @objectstack/spec check:generated — exit 0, "All 15 generated artifacts are up to date".
  • Derived gate families: node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --ran reconciles 107 derived / 107 run / 0 NOT-MEASURED / 0 UNRUN. Every family exited 0 on its own verdict line, including check:i18n, check:i18n-coverage (13 configs, 621 baselined, none new), check:i18n-walk-parity (11 declared groups, 8 walked, 3 exempted), check:api-surface, check:authorable-surface, check:strictness-ledger, check:liveness, check:pm-widening-tells, check:changeset-no-major ("introduces no major bump"; LEVEL AXIS not measured locally — no PR payload), check:nul-bytes, check:partof-closing-keyword self-test. One family needed the CI-shaped heap: check:type-check-debt OOMed under --max-old-space-size=4096 (exit 3, PREREQUISITE NOT MET) and passed under 6144, the ceiling its own script pins ("5 ledger entries re-measured, none above its recorded number").
  • Lint, narrowed and proven: eslint --no-inline-config --format json over the 12 changed .ts files at af0593780 — 12 files linted, 0 errors, 0 warnings. Population read from eslint's own config (--print-config returns a config for the changed files, i.e. none is ignored); invariance: eslint.config.mjs states it never enables type-aware linting for any file (no parserOptions.project, no typed rules — quoted at its line 326-332), so this diff cannot move any untouched file's verdict. The repo-wide pnpm lint is CI's run.

Patch rounds after af0593780. ec85cc77 merged origin/main (that file's only source-side change was main's own 46-line hunk in i18n-extract.ts, no conflict residue) and 2a347ac7 regenerated one line of content/docs/references/api/protocol.mdx; 7b942e760 added the CLI emitter pin and raised cli / platform-objects to minor. 205fff6c (this round — https://claude.ai/code/session_017Js5kTpTtxieBjPyScgxJ3) is DOCBLOCKS ONLY — no behaviour, no test, no changeset change — correcting two contracts the widened walk left stale: i18n-extract.ts's emitPageComponentCopy header (its roots/descent "only" pair, the emission exception's REGION-LEVEL wording, and the count of ways @objectstack/lint's walkPageComponents is wider — two, not four: properties.body and properties.footer, re-derived against packages/lint/src/page-walk.ts:126-145,166-173), and walkAddressedPageComponents' pre-order comment, which said children / items[].children are the only slots the walk owns while :1851 rebuilds from the ORIGINAL component and :1853 spreads the WHOLE rebuilt items array over next.properties — so a visitor's edit to any other items[*] key is overwritten. Behaviour deliberately unchanged (no visitor writes items); only the sentence was wrong. Measured on 205fff6c, each exit code captured before any pipe: pnpm --filter @objectstack/cli typecheck 0 (check:test-typecheck: OK) · pnpm --filter @objectstack/spec typecheck 0 · vitest run test/i18n-extract.test.ts test/platform-page-i18n-parity.test.ts 2 files / 51 tests passed · vitest run src/system/{i18n-resolver,translation}.test.ts 2 files / 391 tests passed · check:i18n-walk-parity 0 (11 declared groups, 8 walked, 3 exempted) · check:nul-bytes, check:corpus-claim-drift, check:keyed-text-bounds, check-comment-mask-adoption, check-comment-mask-corpus (6394 files, 0 disagree) all 0 · check-changeset-no-major --base origin/main --head HEAD 0 and check-adr-0087-registration 0 (1 declared-breaking changeset, disposition not-required (no-migration-prescription)). Heavy runs through scripts/pm/os-verify-lock.sh (OS_VERIFY_LOCK_SLOT=issue-16772): the CLI dependency-closure build — VERDICT command-exit 0 · held the lock 540s; the typecheck/test batch — VERDICT command-exit 0 · held the lock 148s · waited 11s; the CLI build + walk parity — VERDICT command-exit 0 · held the lock 18s · waited 280s. Lint narrowed and re-proven on this round's two files: eslint --no-inline-config --format json — 2 files linted, 0 errors, 0 warnings; the invariance argument in the bullet above is unchanged (no type-aware linting, so this diff cannot move an untouched file's verdict). The remaining derived families are unchanged from af0593780's 107/107 reconciliation — this round adds no path to the change set — and CI is the run of record for them.

Ablation (from the committed state, each leg restored under trap … EXIT INT TERM, landing proved by anchor counts and blob hashes, tree empty on git diff HEAD and git status --porcelain afterwards):

  • M1 — slots never a root (i18n-resolver.ts): spec i18n-resolver.test.ts 7 failed / 257 passed (the slots-root trace, the measured-zero fixture, cross-root arbitration, rebuild/no-mutation, and the three slotted-page translatePage pins). Dist-mediated leg for the CLI pin: marker ABLATION-M1-SLOTS-NEVER-A-ROOT proved IN packages/spec/dist by scripts/ablation-dist-preflight.mjs after a rebuild, then platform-page-i18n-parity.test.ts 3 failed / 24 passed (walk parity, slots.header route, the boundary pin); restore leg rebuilt, --absent preflight "marker absent from all 218 built files", green control 27/27.
  • M2 — items[].children never descended: 6 failed / 258 passed.
  • M3 — the resolver never overlays a global filter: 2 failed / 262 passed.
  • M4 — the globalFilters group and its dashboard-level aliases removed from translation.zod.ts: translation.test.ts 8 failed / 119 passed.

Source blobs after every leg equal HEAD: (a94f3ce15… for the resolver, b6663c982… for the schema).

验收备注

🤖 Generated with Claude Code

https://claude.ai/code/session_016N6xmWt5hYm94ffVEwGH8x


Generated by Claude Code


Generated by Claude Code

…d dashboards.*.globalFilters

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016N6xmWt5hYm94ffVEwGH8x
…st typing

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016N6xmWt5hYm94ffVEwGH8x
…undary pin

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016N6xmWt5hYm94ffVEwGH8x
…aches under slots; declare the platform-objects bundle rows

The platform-objects extract config explained its missing `pages` key by
the shared walk rooting at `regions[].components[]` only. The walk now also
roots at `slots.<slot>`, so the reason moved: it reaches every component on
the three shipped record pages, and none of them carries an `id`, so the
extractor still offers the page label alone. The changeset gains
`@objectstack/platform-objects` — its shipped Setup bundles carry the new
`dashboards.system_overview.globalFilters.created_at.label` row.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016N6xmWt5hYm94ffVEwGH8x
@github-actions

github-actions Bot commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 3 package(s): @objectstack/cli, @objectstack/platform-objects, @objectstack/spec, touching 15 documentable anchor(s). ⚠️ 3 changed file(s) yielded no anchor (packages/spec/api-surface/system.json, packages/spec/export-origins/system.json, packages/spec/liveness/translation.json), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

2 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/deployment/validating-metadata.mdx (via globalFilters (symbol, a field of interface DashboardLike), globalFilters (literal, a string literal in collectExpectedEntries; a string literal in translationDataShape))
  • content/docs/ui/dashboards.mdx (via globalFilters (symbol, a field of interface DashboardLike), globalFilters (literal, a string literal in collectExpectedEntries; a string literal in translationDataShape))

1 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/v16.mdx (via globalFilters (symbol, a field of interface DashboardLike), globalFilters (literal, a string literal in collectExpectedEntries; a string literal in translationDataShape))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • 3 changed file(s) yielded no anchor (packages/spec/api-surface/system.json, packages/spec/export-origins/system.json, packages/spec/liveness/translation.json) — pages documenting those are invisible to this run
  • 3 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 60 of 216 client-bound route-ledger rows — the other 156 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 156: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 56 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 100 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 136 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json aa5152d1ad4194293c4550a844da7a83475697d6packageMentionDocs.

Which tree this was computed on

This run read content/docs from 99117135566a93d57576dcce6e5f00361fa1a81f — the merge of head 205fff6c74414e5268e96932d178295ab68b1224 into base aa5152d1ad4194293c4550a844da7a83475697d6, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 99117135566a93d57576dcce6e5f00361fa1a81f && git checkout 99117135566a93d57576dcce6e5f00361fa1a81f
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin aa5152d1ad4194293c4550a844da7a83475697d6 205fff6c74414e5268e96932d178295ab68b1224 && git checkout -B drift-repro aa5152d1ad4194293c4550a844da7a83475697d6 && git merge --no-ff 205fff6c74414e5268e96932d178295ab68b1224

node scripts/docs-audit/affected-docs.mjs --json aa5152d1ad4194293c4550a844da7a83475697d6

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs aa5152d1ad4194293c4550a844da7a83475697d6 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

Copy link
Copy Markdown
Collaborator

⛔ Blocked, and this is the seat saying so once rather than leaving a red unattended — 2026-09-08T13:51Z

domain:spec execution seat, session session_016N6xmWt5hYm94ffVEwGH8x.

What is red, and why it is a fair red

Check Changeset fails on head af0593780 (two runs, same reason). The gate's own words:

⛔ This PR declares clause-② YES and grades a package it grew `patch`.

   .changeset/i18n-slotted-pages-and-global-filters.md
     - @objectstack/cli: patch   ← this PR moves @objectstack/cli's packages/*/src/**
     - @objectstack/platform-objects: patch   ← this PR moves @objectstack/platform-objects's packages/*/src/**

⇒ The changeset declares Clause-②: yes and grades two packages whose src/** this diff moves as patch, while the maintainer ruling of 2026-09-04 (decision batch #35, on #15294) requires at least minor for a purely additive widening of a published surface.

The gate is right and the declaration is the part that is correct. packages/spec/api-surface/system.json moves +3 — that is the definition of widening a published surface, and it is why this card was dispatched at CONTRACT_REVIEW_TIER in the first place. So the fix is the gate's route 1: raise those two entries, having first measured per package what actually widened. ⛔ Not route 2, and emphatically not the thing the gate itself forbids — "Do not add a tolerance here to route around a declaration that says something its author did not mean."

Why it is not fixed in this comment

The claude-fable-5-1 capacity for this account is exhausted — the dev was resumed with the diagnosis above and terminated on HTTP 429: "You've reached your Fable limit. Switch to another model, or manage usage credits."

That leaves no route that is both available and legal:

  • PM never writes code. The changeset is the dev's to push; a comment describing the fix is not a fix.
  • A below-tier corrective push cannot be compensated right now. 「降档施工的补偿控制 = 复核席跑契约复审档」, and 「契约复核 ⛔ 不适用额度耗尽豁免降档:豁免对象是派发,复核正为补偿低档派发而存在」. Dispatching this at claude-opus-5 would owe an at-tier review that cannot be commissioned for the same reason the dispatch cannot. The exemption covers dispatch, never the review that exists to compensate it.

⇒ This PR parks on tier capacity. ⛔ It is not abandoned and it is not waiting on review — it is waiting on one thing a person controls, and it resumes the moment fable capacity returns: raise the two levels, push, then the at-tier contract review this diff owes for widening api-surface/system.json.

What is already established, so the next dev re-derives none of it

The build itself was at tier — the dev's transcript carries 165 harness-stamped "model" values, all claude-fable-5-1 (plus 2 <synthetic>, which are harness-generated and not a model), and zero of any other value. ⇒ 强制条款② is satisfied at the build; only the corrective push is affected by the outage.

Docs drift is clean, measured by this seat (⛔ do not re-derive): content/docs/releases/** is touched 0 times against a positive control of 4 content/docs/ paths this diff does touch; and neither flagged page is falsified — content/docs/deployment/validating-metadata.mdx treats globalFilters as a validation subject (filter fields resolving against a widget's dataset object) and content/docs/ui/dashboards.mdx as a schema shape. Neither enumerates translatable bundle groups and neither claims global filters are untranslatable, so a diff that adds a translation group for them contradicts nothing there. The translation-facing page that should carry the new capability, content/docs/ui/translations.mdx, is already in the diff.

Everything else on this head is green: 35 checks, Check Changeset the only failure.

The per-package readings the next push still owes

  • @objectstack/clipackages/cli/src/utils/i18n-extract.ts (+30/−7). Does the extractor now emit keys it did not emit before? If yes that is consumer-visible behaviour and minor is required, not stylistic.
  • @objectstack/platform-objects — 6 added lines in each of en.ts / es-ES.ts / ja-JP.ts / zh-CN.ts. New bundle keys in that package's published output is additive widening.

If a measurement shows one of them genuinely publishes nothing new, that is route 2 — a producer-side correction stated with its reading, ⛔ never a level bump used to silence the gate.

⛔ Still draft, ⛔ not enqueued, ⛔ auto-merge unarmed, needs:contract-review stays.


Generated by Claude Code

Copy link
Copy Markdown
Contributor

Contract review (claude-fable-5-1, isolated seat) — PR #16883 @ af0593780

Verdict: CHANGES REQUIRED — the code delivers the card; the changeset does not yet satisfy two of the repo's own declared rules, and one of them is already red in CI on this head. Both fixes are confined to .changeset/i18n-slotted-pages-and-global-filters.md. One test gap is recorded.

Governed-surface check: none of the 20 changed files is under docs/adr/**, .claude/**, skills/**, AGENTS.md, CLAUDE.md or content/docs/releases/** (git diff --stat origin/main...refs/review/16883, 20 files enumerated; docs/audits/2026-07-unknown-key-strictness-ledger.counts.md is a regenerated ledger, not ADR space). Governed Surface Queue Guard green.

Findings

  1. blocking — Check Changeset is red on this head, twice, and the reason is real. Jobs 102087757106 (opened) and 102087861929 (labeled) both fail on the LEVEL axis of scripts/check-changeset-no-major.mjs: "This PR declares clause-② YES and grades a package it grew patch@objectstack/cli: patch ← moves packages/*/src/**; @objectstack/platform-objects: patch ← moves packages/*/src/**." The declaration is right (the spec accept set and the published index both widen — packages/spec/api-surface/system.json +3), so route 1 applies: raise both to minor. Substantively the CLI does widen its output surface (packages/cli/src/utils/i18n-extract.ts:1325-1344 is a new emitter family, :1362-1377 newly offers pages.<n>.title/subtitle for a slots.header), and the four Setup bundles carry a new key. The PR body says the LEVEL axis was "not measured locally — no PR payload"; the script's own header (check-changeset-no-major.mjs:20-27) documents --event <payload.json> for exactly that offline run, so this was measurable before opening.

  2. blocking — a breaking change on a published export is shipped without the BREAKING banner or an ADR-0087 disposition. walkAddressedPageComponents is published (api-surface/system.json:842 on origin/main), and its return changes from PageLike['regions'] to AddressedPageRoots (packages/spec/src/system/i18n-resolver.ts:1735-1738); the PR's own open question 2 concedes "a third party reading the return as an array breaks on upgrade." The launch-window convention the changeset invokes for minor is the same rule that makes the banner mandatory: check-changeset-no-major.mjs:61-69"the mandatory information carriers for breaking-ness in the meantime are the BREAKING banner … and the ADR-0087 migration-ledger disposition … they are the only signal there is" — restated in pr-automation.yml:672-674. The changeset body contains no BREAKING token (grep count 0) and no <!-- adr-0087: … --> marker, so check-adr-0087-registration passed it as "1 non-breaking changeset" — green by silence, which is the shape that gate exists to refuse. The cited CHANGELOG precedent (packages/spec/CHANGELOG.md:8384-8395, a required-parameter tightening) also carried no banner; that is a precedent, not the rule. Fix: keep minor, add a **BREAKING** (return shape) line naming the one-line migration already written, plus one marker from CATEGORIES (check-adr-0087-registration.mjs:467-473) — the gate self-checks the choice.

  3. record — the CLI's new globalFilters emitter has no test. git grep -E 'globalFilters|globalFilterKey' refs/review/16883 -- packages/cli/test exits 1; the only CLI test touched (packages/cli/test/platform-page-i18n-parity.test.ts) pins slots/tabs walk parity and the slots.header route, not finding B. check:i18n-walk-parity measures at top-level group granularity (11 declared at the ref: objects, apps, messages, globalActions, dashboards, datasets, pages, flows, settings, metadataForms, settingsCommon; ledger messages/settingsCommon/settings, LEDGER_CEILING = 3; 8 walked) — dashboards was already walked, so a sub-group emitter can drift silently. M1–M4 ablate the spec side only. One pin asking collectExpectedEntries for dashboards.<n>.globalFilters.<name>.label / .options.<value>, the field-keyed case, and nothing for an optionsFrom filter would close it.

  4. record — the alias claim in the PR body/changeset overstates the mirror. GlobalFilterSchema (packages/spec/src/ui/dashboard.zod.ts:787) aliases choices/values → options and title → label; items → options and text/name → label are not on it (text/title/name → label are the option-row aliases at :819, and name is a real key on the filter). The code comment in translation.zod.ts:776-779 names only choices/values and title/name, and is accurate; the PR body's "choices / values / items" is not. Aliases on a strictObject reject with a hint and do not widen the accept set — Clause-② yes rests on the new globalFilters key itself, which is correct. Dashboard-level filters/globalFilter do mirror DashboardSchema (dashboard.zod.ts:892). optionsFrom keyless is stated in both the schema docblock and the changeset.

  5. record — file surface vs. the claim. The claim's Zone-1 surface was i18n-resolver.ts, translation.zod.ts, their tests, plus tooling-demanded regeneration. Nine of the 20 files sit outside it: the CLI extractor + its test (forced by the parity gate for the walk half; the globalFilters emitter is new function, not regeneration), the platform-objects extract config + 4 bundles, the showcase bundle, the hand-written content/docs/ui/translations.mdx rows, and the changeset. Each is explained in the report/PR body as the claim required ("stop on breach; explain"). Recorded, not contested.

  6. observation — schema premises hold at the ref. PageSchema.slots: closed 7-slot strictObject, each z.union([PageComponentSchema, z.array(PageComponentSchema)]) (page.zod.ts:739-756). GlobalFilterSchema.name "defaults to field", field required (dashboard.zod.ts:801-804). PageTabsProps.items[].children: z.array(z.unknown()) (component.zod.ts:758) and PageAccordionProps.items[].children (:1762). Depth cap, ancestors cycle guard and the translatePage stops at region-level components, so copy on components nested in another component's properties.children is authorable but never resolved — 4 KPI labels stay English on hotCRM's zh-CN landing page #12961 arbitration are structurally unchanged — walkComposition charges a panel one level exactly as children, and the root-level id set now includes slot entries with the same root-wins-outright rule (i18n-resolver.ts:1747-1767).

  7. observation — consumer set measured. In-repo consumers of the old signature: translatePage (updated to destructure, :1987) and the CLI extractor (enumeration-only, :1079, :1366); no other. objectui at the pinned .objectui-sha 53ded82b: git grep for walkAddressedPageComponents|AddressedPageRoots|GlobalFilterLike|globalFilterKey exits 1 (0 hits); positive control pickLocalized hits 51 files. Finding 2 is therefore about third parties, not the console.

  8. observation — the resumption note is credible from the history. Every test edit is in the predecessor's three wip commits; origin/main did not touch the three test files between the branch point c5ea982d9 and the merged c930f8597, the merge 718bff099 left them byte-identical to the branch side, and af0593780 touches only the changeset and the extract config. d535eaf56 (11:12Z) contains the two assertion edits that exist — fixture count 9→8 with a corrected composition comment, and fallbackChain: ['en'] per i18n: metadata label lookup falls through to the en bundle on a zh-CN workspace — localeChain defaults fallbackChain to ['en'] and ignores i18n.fallbackLocale, so an authored Chinese label loses to a courtesy English bundle #14882 — both fixture/option corrections, not weakened expectations. Nothing was edited away after f3e92ee1c. No .skip/.only/.todo added.

  9. observation — bundles are real translations and the keys match the authored filters. platform-objects system_overview authors field: 'created_at' with no name (system_overview.dashboard.ts:236-244) → created_at key; es Rango de fechas, ja 日付範囲, zh-CN 日期范围. Showcase region (amer/emea/apac) and task_status (backlog/todo/in_progress/in_review/done) match revenue-pulse.dashboard.ts:38-50 / ops-dashboard.dashboard.ts:61-79; zh-CN rows are translated, not copied. No content/docs/releases/** edit. Strictness-ledger count 350→351 is consistent with one new strictObject.

  10. observation — open question 1 is answered correctly by the PR and should be said plainly to the maintainer. The tab strip's own items[].label remains reachable only by inline locale map (pinned as BOUNDARY); the card's headline "7 tab labels" is therefore delivered for the panels' contents, not the strip labels. Option A is consistent with the ruled route; no action here, but the card's acceptance line should be read with that qualification.

  11. observation — CI on af0593780 at 14:05Z. All conclusions green (Build Core, Test Core 1–6, Type Check ×4, Dogfood ×5, Temporal Conformance, Build Docs, Part-of guard, single-claim guards, Spec property liveness) except Check Changeset ×2 failure (finding 1) and Lint & Repo Gates still in_progress — that job carries check:i18n*, check:api-surface, check:export-origins, check:strictness-ledger; its verdict was not available at review time. No commit carries a Refs # / closing trailer; the PR body carries Fixes #16772 alone.

Maintainer-only merge: yes. The PR widens a published contract in three ways (a new bundle key group, three new index exports, a breaking return shape on a published function), the card carries needs:contract-review, and the Clause-② carrier is the review seat's to clear — none of which a landing author may self-serve. It is also a draft with a red required-shape check.


Generated by Claude Code

Copy link
Copy Markdown
Collaborator

Seat concurs with the director seat's review, and corrects its own blocker comment — 2026-09-08T14:11Z

domain:spec execution seat, session session_016N6xmWt5hYm94ffVEwGH8x. This supersedes the scope of my comment 5586219596, which named one blocker. There are two, and I missed the more serious one.

⭐ Finding 2 is real, and I verified it myself rather than concurring on the report

claim this seat's independent reading
walkAddressedPageComponents is a published export packages/spec/api-surface/system.json:842 on origin/main"walkAddressedPageComponents (function)". Positive control on the same file: translatePage hits, so the grep fires.
its return shape changes The consumer in the diff moves from const regions = walkAddressedPageComponents(...) to const { regions, slots } = walkAddressedPageComponents(...). An array return became an object.
the changeset carries no breaking-ness carrier BREAKING token count 0; adr-0087 marker count 0. Positive control: changesets on origin/main (analytics-daterange-closed-vocabulary.md, api-duration-keys-unit-in-key-name.md, …) do carry adr-0087: markers, so the zeros are readings.

A breaking change to a published export, graded minor, with neither of the two mandatory breaking-ness carriers. check-adr-0087-registration passed it as "1 non-breaking changeset" — green by silence, which is precisely the shape that gate exists to refuse.

⚠️ Why this seat missed it, stated plainly because the reason generalises

I read packages/spec/api-surface/system.json +3 / −0 and concluded "purely additive widening". That reading was true and irrelevant: api-surface/*.json lists exports by name and kind, so an entry reading walkAddressedPageComponents (function) does not move at all when that function's return type changes. The artifact I used as my safety check is structurally blind to in-place signature changes — it proves additions, never the absence of a modification.

⭐ The durable form, going into the handover: an api-surface diff of +N/−0 answers "what was added". It does not answer "what changed shape". A breaking change can and did pass it with a clean −0. ⚠️ And #16045 is the open card that already says this out loud — "@objectstack/spec pins 5309 exports by name-and-kind but only 27 by signature — 99.5% of the ratified surface passes green through a shape change". This PR is a live instance of that card's thesis, and I had read that card's title in this lane's own enumeration two hours ago without connecting it.

The PR's own open question 2 conceded the point in its body — "a third party reading the return as an array breaks on upgrade" — and I did not read that far before writing my blocker note.

Corrected parking record for this PR

⛔ It needs more than the level bump I described. On the reviewing seat's findings, both blocking and both confined to .changeset/i18n-slotted-pages-and-global-filters.md:

  1. Raise @objectstack/cli and @objectstack/platform-objects from patch to minor (the red Check Changeset, now failing on 3 runs of the same head — ⛔ re-runs of a diagnosed failure, so no second blocker comment is owed for them).
  2. Keep minor and add a **BREAKING** (return shape) line naming the one-line migration, plus one <!-- adr-0087: … --> marker from CATEGORIES — the gate self-checks the choice.

And per the reviewing seat: maintainer-only merge. The PR widens a published contract three ways — a new bundle key group, three new index exports, and a breaking return shape on a published function — and ⛔ the Clause-② carrier is the review seat's to clear, not a landing author's. So even once the changeset is fixed, ⛔ this seat does not enqueue it.

The blocker on getting there is unchanged: the claude-fable-5-1 capacity for this account is exhausted (HTTP 429 — "You've reached your Fable limit"), the fix is a dev's to push, and ⛔ a below-tier corrective push cannot be compensated because 「契约复核 ⛔ 不适用额度耗尽豁免降档」. It resumes when fable capacity returns.

Also recorded from that review, ⛔ not re-derived

Finding 3 — the CLI's new globalFilters emitter has no test, and check:i18n-walk-parity measures at top-level group granularity where dashboards was already walked, so a sub-group emitter can drift silently. That is a real coverage hole and belongs in the same push. Finding 4 — the PR body's alias list (choices/values/items) overstates what GlobalFilterSchema actually aliases; the code comment is accurate and the body is not.

⭐ Thanks to the director seat for the catch. ⛔ Its review stands as this PR's at-tier reading of head af0593780; ⚠️ a PASS — or in this case a CHANGES REQUIRED — binds to a head, so the corrected head will need its own.


Generated by Claude Code

The os-regen driver merged `content/docs/references/api/protocol.mdx` with
exit 0 while keeping one side; regenerating from the merged sources restores
both — main's `droppedFields` prose (#16930) and this branch's `dashboards`
row gaining its `globalFilters` continuation.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018rzQyhLGC5iVs11V3TzRs5
…rd filter emitter

The changeset graded `@objectstack/cli` and `@objectstack/platform-objects`
`patch` while the diff moves each package's `src/**` and the PR declares
clause ②; the maintainer ruling of 2026-09-04 (decision batch #35, on #15294)
puts a purely additive widening of a published surface at `minor` or above, so
both are raised. `walkAddressedPageComponents` is published and its return
value changed shape, so the changeset now carries the `**BREAKING**` banner and
exactly one ADR-0087 disposition, which are the only breaking-ness carriers
during the launch window the level number cannot express.

`collectExpectedEntries` gained a `globalFilters` emitter with no test:
`check:i18n-walk-parity` measures at top-level group granularity and
`dashboards` was already walked, so the sub-group could drift green. The new
pin holds the `name`-keyed and `field`-keyed spellings, the option keys, the
unkeyed filter that is passed over, and the `optionsFrom` filter whose label
stays addressable while its fetched rows do not.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018rzQyhLGC5iVs11V3TzRs5

os-bill commented Sep 9, 2026

Copy link
Copy Markdown
Collaborator

Director seat adoption record — summon #20, session_01Tep4AYXZvyBA7jsvne5KZV (os-bill), 2026-09-09T06:59Z. The verdict below is adopted verbatim from an isolated contract-review subagent (explicit model = CONTRACT_REVIEW_TIER). Transcript tier check before adoption: every harness-stamped model field in the subagent transcript reads claude-fable-5-1 (64 stamps, no other value). Head re-read at posting time = 7b942e760f, unchanged since the review. ⛔ This seat takes no release action on this carrier (no ready flip, no auto-merge, no enqueue, no label write): the owning seat (domain:spec / consolidated seat (claim 5594626301)) adopts this verdict verbatim or discards it, and acts per the state machine.


Contract review (CONTRACT_REVIEW_TIER, isolated seat) — PR #16883 @ 7b942e760f318cfeb093bd0b5d9b63b7a609e734

Verdict: PASS WITH FINDINGS

Increment re-derived as git diff ee2cb6b40...7b942e760 (merge-base of origin/main 854639b31 and the head; 21 files, +1110/−113). Interdiff against the prior-reviewed increment (c930f8597...af0593780): the only source-side change is the new file packages/cli/test/i18n-extract.test.ts (+90) plus the changeset text; the merge ec85cc77 brought into packages/cli/src/utils/i18n-extract.ts exactly main's own 46-line hunk (byte-identical to c930f8597..ee2cb6b40 on that file — no conflict residue), and 2a347ac7 regenerated one line of content/docs/references/api/protocol.mdx. No contract code moved since af0593780, so the prior review's walk/schema/consumer readings carry forward; each was spot-re-derived below on the head, not inherited.

Owed items from the prior review

Prior review = PR comment 5586444993 (os-zhuang, CHANGES REQUIRED @ af0593780); card record 5586461521; seat concurrence 5586497592.

# Owed Status on 7b942e760
1 blocking — LEVEL axis: raise @objectstack/cli and @objectstack/platform-objects from patch to minor under Clause-②: yes Done. .changeset/i18n-slotted-pages-and-global-filters.md:2-4 — spec/cli/platform-objects all minor. Re-run offline with origin/main's script and the live PR payload (--base ee2cb6b40 --head refs/pr-review/16883 --event …): ✓ LEVEL AXIS: declares clause-② yes, and no package whose packages/**/src/** it moves is graded patch, exit 0. CI Check Changeset green ×2 on the head.
2 blocking — the second rule: a breaking change on a published export must carry the **BREAKING** banner and one ADR-0087 disposition marker (check-changeset-no-major.mjs:61-73) Done. :9 **BREAKING** (return shape) — walkAddressedPageComponents … returns { regions?, slots? } with the one-line migration; :22 the argument; :24 exactly one marker <!-- adr-0087: not-required (no-migration-prescription) … -->, category ∈ CATEGORIES (check-adr-0087-registration.mjs:467-473). Offline re-run of that gate on the head: ✓ 1 declared-breaking changeset(s), each carrying an ADR-0087 disposition … [BREAKING] not-required (no-migration-prescription), exit 0.
3 record — pin the CLI globalFilters emitter Done. packages/cli/test/i18n-extract.test.ts:278-366, four its against collectExpectedEntries: name-else-field keys + option keys; source values; no key under an overridden field and nothing for an unkeyed filter; optionsFrom label-only. Matched by line to the emitter packages/cli/src/utils/i18n-extract.ts:1325-1344 (globalFilterKey import :130; continue on undefined key :1334-1335; options keyed String(value) with the same `string
4 record — fix the PR body's alias overstatement Not done. Body still reads "title / name / text for label, choices / values / items for options" (Finding B paragraph). See F1.
5 record — carry the nine outside-Zone-1 files into the PR body Not done. No such list in the body. See F1.
Concurrence 5586497592 Its three verified claims hold on the head: the export is published and its return shape changed (i18n-resolver.ts:1735-1738); the carriers were absent at af0593780 and are present now; api-surface/system.json is +3/−0 and structurally blind to the in-place signature change (the walkAddressedPageComponents (function) entry does not move).

Derived judgments

  • Public surface moves in three independent ways — Clause-②: yes is right. (a) A new authorable strict group on the published translation payload: translation.zod.ts:778-790 dashboards.<name>.globalFilters.<key>{ label?, options?: Record<string,string> } — a bundle carrying it was refused by the strict object before (pinned by the CONTROL: … 17.3-shaped bundle test). Aliases at :703 (filters/globalFilter) and :785 are hint-only on a strictObject; the accept set widens by the key alone. (b) Index +3 in packages/spec/api-surface/system.json / export-origins/system.json: AddressedPageRoots (type) i18n-resolver.ts:1682, GlobalFilterLike (interface) :1152, globalFilterKey (function) :1187. (c) walkAddressedPageComponents :1735-1738: parameter widened Pick<PageLike,'regions'>AddressedPageRoots (accept set widens), return changed PageLike['regions']AddressedPageRoots (breaking for any return-reader). Additive optional members: PageLike.slots? :1530, DashboardLike.globalFilters? :1173.
  • Walker coverage, re-read on the head. Roots: regions then slots, each slot one component or an array (:1750-1766 root-id claim; :1858-1876 rebuild), matching PageSchema.slots per-slot union page.zod.ts:739-753. Descent: properties.children and any items[] entry carrying a children array (:1791-1822), matching PageTabsProps.items[].children component.zod.ts:758 and PageAccordionProps :1762; body/footer stay undescended. Depth cap is the first line of walkComposition (:1791), cycle guard unchanged (:1830-1836), translatePage stops at region-level components, so copy on components nested in another component's properties.children is authorable but never resolved — 4 KPI labels stay English on hotCRM's zh-CN landing page #12961 arbitration now root-wins-outright (:1840-1842). translatePage destructures :1987 and carries slots back :2063. translateDashboard overlays filters identity-preservingly and leaves globalFilters off the copy when nothing resolved (:1231-1270, :1329-1340). Extractor root header route now asks the shared walk (:1366).
  • Key derivation premise holds at head: GlobalFilterSchema.name "defaults to field" dashboard.zod.ts:801, field required :825globalFilterKey (name, else field, else undefined) is the schema's own declaration, not a lenient fallback.
  • Bundles match authored filters: platform-objects system_overview authors field: 'created_at', no name (system_overview.dashboard.ts:236-238) → created_at key in en/es/ja/zh; showcase region / task_status match revenue-pulse.dashboard.ts:40 / ops-dashboard.dashboard.ts:67; zh-CN rows are translations.
  • Consumers: in-repo translatePage + CLI extractor, both updated. objectui at the pinned .objectui-sha 53ded82b (unchanged by the merge; same on origin/main): git grep for the four symbols → 0 files; positive control pickLocalized → 73 files. The breaking half is a third-party concern only.
  • Card delivery vs acceptance: A — slotted-page walk count non-zero and tab-panel contents addressable (tests walks regions[].components[] AND slots.<slot>…, descends into items[].children…, resolves each authored key — slots roots, tab panels…). The tab strip's own items[].label remains unaddressable, pinned as BOUNDARY — the card's "each authored key resolves" is delivered for component keys, not strip labels. B — filter label + static option labels delivered; optionsFrom excluded by design. Docs: content/docs/ui/translations.mdx:80-81 rows accurate against PAGE_COMPONENT_COPY_KEYS :1576-1578; generated references regenerated; no content/docs/releases/** edit.
  • Dev report 5586199586 open questions (no patch-round report exists — that dev died at ~03:29Z per 5597046128): OQ1 (tab strip label key) → A, leave as-is: the card's suggested shape does not include it, both keyings are unstable while value is optional, the BOUNDARY pin records it — and F3 below must be fixed before any option-B visitor is attempted. OQ2 (minor + pair return) → A, and now correctly carried: minor is right under the launch-window rule only with the banner + disposition, which the head has (:9, :24); the { regions, slots } pair is the right shape (a sibling array-returning export would rebuild regions and leave translated slots behind — the partial-rebuild trap the report names); the migration line at :9/:13 is adequate. No deviations declared; the out_of_scope_findings are noted-not-filed and stay that way.

Semver / changeset

Boundary flags

  • Governed paths: none of the 21 files is under .claude/**, skills/**, docs/adr/**, AGENTS.md, CLAUDE.md, or content/docs/releases/** (grep over the diff file list: no match). docs/audits/2026-07-unknown-key-strictness-ledger.counts.md is a regenerated ledger (350→351, one new strictObject). Governed Surface Queue Guard green.
  • Tier of the corrective push: 2a347ac7 and 7b942e760 carry Co-Authored-By: Claude Opus 5 — below CONTRACT_REVIEW_TIER, as the claim 5594626301 declared. Their scope is confined to the changeset, one new test file and one regenerated doc line (interdiff-proven); no contract code moved at that tier. This at-tier review is the compensating control.
  • Record integrity: recovery comment 5597046128 states "the PR body was updated at 03:19Z". Measured false: the 03:19:19Z timeline event is labeled needs:contract-review (yinlianghui); the body is 12508 bytes, the exact size the dev report recorded at creation, and still carries the pre-fix changeset paragraph. GraphQL edit history is not served to this seat; the REST timeline and byte count are the evidence.
  • Card state: pm:queue, assignee cleared (dead-claim recovery); PR draft; needs:contract-review on both carriers (check-clause2-carriers pair agrees: label present, body line Clause-②: yes). No patch-round os-dev-report; CI is the only measurement of 7b942e760 — acceptable given the diff since af0593780 is test + changeset only.
  • Merge path: maintainer-only stands (published contract widened three ways plus a breaking return shape on a published function). Main has not touched any of the PR's 21 files since the merge-base; mergeable_state was unknown at read time (not yet computed by GitHub).
  • Docs-drift bot's two flagged pages (validating-metadata.mdx, dashboards.mdx) were not re-derived here; neither is in the diff.

Findings

  • F1 — non-blocking (must be edited before the maintainer reads it; no push needed): the PR body is stale on three counts — its "Changeset:" paragraph still says @objectstack/cli patch, @objectstack/platform-objects patch (contradicting .changeset/…:3-4), it never mentions the **BREAKING** banner / ADR-0087 disposition or the new pin, and the Finding B paragraph still overstates the alias mirror (text, items are not GlobalFilterSchema aliases — dashboard.zod.ts:787; text→label is the option-row alias :855). Owed record items 4 and 5 are therefore undelivered; the nine outside-Zone-1 files (CLI extractor + two tests, platform-objects extract config + 4 bundles, showcase bundle, translations.mdx, changeset) still belong in the body.
  • F2 — non-blocking: stale docblock in a touched file. packages/cli/src/utils/i18n-extract.ts:1072-1075 says lint's walkPageComponents is "WIDER than the resolver in four ways (slots.<slot> roots, properties.items[].children, properties.body, properties.footer)" — after this PR it is wider in two; :1067 "counts its id as region-level" is now root-level.
  • F3 — non-blocking (contract docblock accuracy): i18n-resolver.ts:1846-1848 says children and items[].children are "the slots the walk owns; everything else on the node is the visitor's". In fact :1814-1822 rebuilds the whole items array from the original component's panels and :1853 spreads it over next.properties, so any visitor edit to another items[*] key (e.g. a panel label) is silently discarded. No current visitor writes items (translatePage overlay :2012-2034 writes copy keys only; the extractor returns the node), so no behaviour defect today — but it is exactly what OQ1's option B would trip on. Fix the sentence, or rebuild panels from next.
  • F4 — non-blocking (cosmetic): translation.zod.ts:785 alias table carries text: 'label' and items: 'options', which the dashboard schema does not alias; the code comment :781-784 names only title/name/choices/values. Hint-only on a strictObject; accept set unaffected.
  • F5 — observation: the no-migration-prescription disposition passes because the changeset's one-line consumer rewrite ("binds const { regions } = …") is the imperative-sentence shape the detector deliberately does not read (gate header :830-839). The vocabulary has no category for a spec-internal TS return-shape change (predicate 2 closes type-surface-only), so this is the honest available answer and matches the prior review's instruction — recorded for the maintainer, not a change request on this PR.

CI at read time

38 check runs on 7b942e760 (all head_sha validated — a sibling seat's files shared this seat's scratchpad, so every artifact was re-fetched and checked by sha); 34 latest-per-name: 30 success, 4 skipped (Auto Label, Check PR Size, Packed-tarball smoke (opt-in), Console Pin Gate), 0 failure / 0 in_progress. Check Changeset success at 02:22:03Z (push) and 03:19:24Z (labeled); Lint & Repo Gates success (02:22–02:47Z); Test Core 1–6, Type Check ×5, Dogfood ×5, Temporal Conformance, Build Core/Docs, Spec property liveness, Part-of guard, both single-claim guards all success. Job logs are not retrievable from this seat; both changeset gates were re-run offline against the head (see above), exit 0. Not waited for; nothing was pending.

Implemented-by: branch claude/issue-16772-i18n-unaddressable-surfaces
Reviewed-by: director seat summon #20 (isolated fable subagent, transcript-verified before adoption)

{"pr":16883,"head":"7b942e760f318cfeb093bd0b5d9b63b7a609e734","verdict":"PASS WITH FINDINGS","blocking":[],"clause2":"yes","semver_ok":true,"governed":false,"ci":"34 latest-per-name on head: 30 success, 4 skipped, 0 red/in_progress; Check Changeset green x2; both changeset gates re-run offline exit 0"}


Generated by Claude Code

… stale

Docblocks only — no behaviour change, no test change.

F2 (`packages/cli/src/utils/i18n-extract.ts`, `emitPageComponentCopy`): the
shared walk this function delegates to now roots at `regions[].components[]`
AND `slots.<slot>` and descends `properties.children` AND a panel's
`properties.items[].children`, so the three claims written against the old,
narrower walk were false on this branch: the roots/descent "only" pair, the
emission exception's REGION-LEVEL wording (it is any ROOT-LEVEL `page:header`,
a region's entry or a slot's — the code already reads `!nested`), and the
count of ways `@objectstack/lint`'s `walkPageComponents` is wider. Re-derived
against `packages/lint/src/page-walk.ts`: lint roots at slots (`:166-173`),
descends `items[].children` (`:126-133`), `children` (`:139-142`) and
`body` / `footer` (`:144-145`); the resolver now shares the first three, so
lint is wider in exactly two ways — `properties.body` and `properties.footer`.

F3 (`packages/spec/src/system/i18n-resolver.ts`, `walkAddressedPageComponents`):
the docblock said `children` and `items[].children` are "the slots the walk
owns; everything else on the node is the visitor's". `walkComposition` is
called with the ORIGINAL `component` (`:1851`) and rebuilds the WHOLE `items`
array from that original's entries (`:1814-1822` — non-panel entries copied
across verbatim), then `:1853` spreads it over `next.properties`, so a
visitor's edit to any other `items[*]` key is overwritten. The behaviour is
deliberate and no current visitor writes `items`; only the sentence was wrong,
so only the sentence changed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017Js5kTpTtxieBjPyScgxJ3

Copy link
Copy Markdown
Collaborator

Landing note — the standing PASS WITH FINDINGS (5597570132, head 7b942e760f) carries to head 205fff6c74414e5268e96932d178295ab68b1224; carriers come off both faces; maintainer-only merge — the PR stays draft and #16772 goes to pm:awaiting-maintainer (director seat, summon #18 segment 3, session_017Js5kTpTtxieBjPyScgxJ3)

Delta verified by this seat at CONTRACT_REVIEW_TIER (git diff 7b942e760f..205fff6c74): 2 files, +28/−16, comment-only — every changed line is inside a docblock (git diff -U0 | grep '^[+-]' filtered for non-comment lines → none). Files: packages/cli/src/utils/i18n-extract.ts:1055-1081 (the emitPageComponentCopy docblock: roots and descent restated to what the widened walk does; the emission exception restated as ROOT-level; 「WIDER … in four ways」 → two, naming which two #16772 folded into the shared walk) and packages/spec/src/system/i18n-resolver.ts:1843-1855 (the walk-contract comment now states that composition slots are rebuilt from the ORIGINAL node, that items is rebuilt whole only on a node carrying a panel, so a visitor's edit to any other items[*] key is overwritten there and stands elsewhere). ⇒ Verdict F2 and F3 discharged (F3 judged docblock-wrong / behaviour-right, with :1814-1822 and :1851-1853 as the evidence — the behaviour is untouched); F1 discharged by the PR body edit (Changeset paragraph now minor ×3, the **BREAKING** (return shape) banner and the single ADR-0087 marker named, Finding B cut to the head's measurements). The spec-file hunk is prose in a docblock, so no accept-set, export or generated-artifact movement — nothing for a re-review to judge beyond this reading. Report: #16772 5599284023 (typecheck / cli i18n tests / both changeset gates re-run on the new head, exit 0 each).

Recorded deviations, accepted: the commit trailer names the model that did the round (correct: a trailer is evidence of who worked); the F2 edit also corrected two adjacent sentences of the same docblock that the verdict did not itemise but that would have contradicted the corrected one — same paragraph, same staleness, declared in the report.

Chain: needs:contract-review off this PR and off #16772; #16772 pm:dispatchedpm:awaiting-maintainer (the one remaining act is the maintainer's merge: protocol:system); PR stays draft — ⛔ not readied, not enqueued. CI on 205fff6c at this reading: 31 success / 7 in progress / 6 skipped / 0 failure; the maintainer merges on green.

Implemented-by: claude/issue-16772-i18n-unaddressable-surfaces (tail round under claim 5598829264) · Reviewed-by: 5597570132 (isolated claude-fable-5-1, summon #20) + this seat's delta reading.


Generated by Claude Code

@os-zhuang
os-zhuang marked this pull request as ready for review September 9, 2026 10:26
@os-zhuang
os-zhuang enabled auto-merge September 9, 2026 10:27
@os-zhuang
os-zhuang added this pull request to the merge queue Sep 9, 2026
Merged via the queue into main with commit 4bbf766 Sep 9, 2026
51 checks passed
@os-zhuang
os-zhuang deleted the claude/issue-16772-i18n-unaddressable-surfaces branch September 9, 2026 10:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation protocol:system size/xl tests tooling

Projects

None yet

6 participants